//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  AutoDiscover.cs
//    
// Abstract:
//
//  The AutoDiscover class. This class is a proxy to the Exchange auto-discovery service. It is used for finding the 
//  CAS server for Exchange Web Services.
//  
//--

using System;
using System.Diagnostics;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler
{
    /// <summary>
    /// Proxy class to Exchange AutoDiscover service.
    /// </summary>
    public static class AutoDiscover
    {
        // Guid for SCP URL keyword
        private const string ScpUrlGuidString = @"77378F46-2C66-4aa9-A6A6-3E7A48B19596";

        // Guid for SCP pointer keyword
        private const string ScpPtrGuidString = @"67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68";

        // AutoDiscover path
        private const string AutoDiscoverPath = @"/autodiscover/autodiscover.xml";

        internal const string OutlookRequestNamespace = "http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006";
        internal const string OutlookResponseNamespace = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
        internal const string AutoDiscoverResponseNamespace = "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006";

        /// <summary>
        /// AutoDiscover the configurations for a specific e-mail address.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="emailAddress">SMTP address</param>
        /// <param name="nc">Network credentials, if null, then current credentials are used</param>
        /// <param name="maxHops">Maximum number of Active Directory and e-mail address redirections</param>
        /// <param name="allowSelfSignedCertificates">Allow self-signed certificates flag</param>
        /// <returns>AutoDiscover response</returns>
        internal static AutoDiscoverResponseXml Discover(
            Context context,
            string emailAddress,
            System.Net.NetworkCredential nc,
            ref int maxHops,
            bool allowSelfSignedCertificates)
        {
            CheckAndDecrementHops(context, ref maxHops);

            // Verify parameters.
            if (emailAddress == null)
            {
                throw new System.ArgumentNullException("emailAddress");
            }

            string[] emailAddressParts = emailAddress.Split('@');

            if ((emailAddressParts.Length < 2) || (string.IsNullOrEmpty(emailAddressParts[1])))
            {
                throw new System.ArgumentException("Invalid format for e-mail address: domain is not specified");
            }

            // Get the domain from the e-mail address.
            string domainName = emailAddressParts[1];

            // Get either a list of SCP URLs or a list with 0 entries.
            System.Collections.Generic.List<string> urls = GetScpUrlList(context, domainName, null, ref maxHops);

            // Create the AutoDiscover response.
            AutoDiscoverResponseXml autoDiscoverResponse = null;

            // Try SCP URLs retrieved from Active Directory to make AutoDiscover requests.
            if (urls.Count > 0)
            {
                // Get the AutoDiscover response with client configuration information.
                autoDiscoverResponse = DiscoverTryUrls(
                    context,
                    emailAddress,
                    nc,
                    urls,
                    true,
                    allowSelfSignedCertificates,
                    ref maxHops);

                // If a valid Outlook response is returned, stop the search.
                if (IsOutlookDataReturned(autoDiscoverResponse))
                {
                    return autoDiscoverResponse;
                }
                else
                {
                    // Clear the list of URLs to try two default URLs.
                    urls.Clear();
                }
            }

            // Add two SSL URLs built from the domain part of SMTP address.
            urls.Add("https://" + domainName + AutoDiscoverPath);
            urls.Add("https://autodiscover." + domainName + AutoDiscoverPath);

            // Try two SSL URLs.
            autoDiscoverResponse = DiscoverTryUrls(
                context,
                emailAddress,
                nc,
                urls,
                false,
                allowSelfSignedCertificates,
                ref maxHops);

            // If no response is returned yet
            if (!IsOutlookDataReturned(autoDiscoverResponse))
            {
                // Try non-SSL URL to get SSL redirect URL.
                string redirectUrl = GetRedirectUrl(context, domainName);

                // If returned redirectUrl is not null it is a valid redirect SSL URL.
                // Then attempt to request AutoDiscover data.
                if (!string.IsNullOrEmpty(redirectUrl))
                {
                    urls.Clear();
                    urls.Add(redirectUrl);

                    autoDiscoverResponse = DiscoverTryUrls(
                        context,
                        emailAddress,
                        nc,
                        urls,
                        false,
                        allowSelfSignedCertificates,
                        ref maxHops);
                }
            }

            return autoDiscoverResponse;
        }

        /// <summary>
        /// Search Active Directory for any related SCP URLs for a given domain name.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="domainName">Domain name to search for SCP information</param>
        /// <param name="ldapPath">LDAP path to start the search</param>
        /// <param name="maxHops">The number of remaining allowed hops</param>
        private static System.Collections.Generic.List<string> GetScpUrlList(
            Context context,
            string domainName,
            string ldapPath,
            ref int maxHops)
        {
            CheckAndDecrementHops(context, ref maxHops);

            string scpUrl = null;
            string fallBackLdapPath = null;
            string rootDsePath = null;
            string configPath = null;

            // The container for SCP pointers and URLs objects from Active Directory
            System.DirectoryServices.SearchResultCollection scpDirEntries;

            // The list of SCP URLs.
            System.Collections.Generic.List<string> scpUrlList =
               new System.Collections.Generic.List<string>();

            try
            {
                // Get the LDAP root path.
                rootDsePath = (ldapPath == null) ? "LDAP://RootDSE" : ldapPath + "/RootDSE";

                // Get the root directory entry.
                System.DirectoryServices.DirectoryEntry rootDseEntry =
                   new System.DirectoryServices.DirectoryEntry(rootDsePath);

                // Get the configuration path.
                configPath = rootDseEntry.Properties["configurationNamingContext"].Value as string;

                // Get the configuration entry path.
                System.DirectoryServices.DirectoryEntry configEntry =
                   new System.DirectoryServices.DirectoryEntry("LDAP://" + configPath);

                // Use the configuration entry path to create a query.
                System.DirectoryServices.DirectorySearcher configSearcher =
                   new System.DirectoryServices.DirectorySearcher(configEntry);

                // Filter for AutoDiscover SCP URLs and SCP pointers.
                configSearcher.Filter =
                   "(&(objectClass=serviceConnectionPoint)" +
                   "(|(keywords=" + ScpPtrGuidString + ")(keywords=" + ScpUrlGuidString + ")))";

                // Identify properties to retrieve using the the query.
                configSearcher.PropertiesToLoad.Add("keywords");
                configSearcher.PropertiesToLoad.Add("serviceBindingInformation");

                // Query Active Directory for SCP entries.
                scpDirEntries = configSearcher.FindAll();
            }
            catch (System.Exception e)
            {
                Logger.WriteTraceWarning(context, e, "!! LDAP call failed");

                // Re-throw if the exception is irrecoverable
                Helpers.FilterIrrecoverableExceptions(e);

                // Return the list with 0 entries.
                return scpUrlList;
            }

            // Identify the domain to match.
            string domainMatch = "Domain=" + domainName;

            // Contains a pointer to the LDAP path of a SCP object.
            string scpPtrLdapPath;

            foreach (System.DirectoryServices.SearchResult scpDirEntry in scpDirEntries)
            {
                System.DirectoryServices.ResultPropertyValueCollection entryKeywords = scpDirEntry.Properties["keywords"];

                // Identify SCP pointers.
                if (Contains(entryKeywords, ScpPtrGuidString))
                {
                    // Get the LDAP path to SCP pointer.
                    scpPtrLdapPath = scpDirEntry.Properties["serviceBindingInformation"][0] as string;

                    // If the SCP pointer matches the user's domain, then restart search from that point.
                    if (Contains(entryKeywords, domainMatch))
                    {
                        // Stop the current search, start another from a new location.
                        return GetScpUrlList(context, domainName, scpPtrLdapPath, ref maxHops);
                    }
                    else
                    {
                        // Save the first SCP pointer ldapPath for a later call if a SCP URL is not found.
                        // Directory entries with a SCP pointer should have only one keyword=ScpPtrGuidString.
                        if ((entryKeywords.Count == 1) && string.IsNullOrEmpty(fallBackLdapPath))
                        {
                            fallBackLdapPath = scpPtrLdapPath;
                        }
                    }
                }
            }

            // Get the computer's current site.
            string computerSiteName = null;

            try
            {
                computerSiteName = GetSiteName();
            }
            catch (System.Exception e)
            {
                Logger.WriteTraceWarning(context, e, "-! Info: unable to get computer site name");

                // Re-throw if the exception is irrecoverable
                Helpers.FilterIrrecoverableExceptions(e);
            }

            if (!string.IsNullOrEmpty(computerSiteName))
            {
                // Search for SCP entries.
                string sitePrefix = "Site=";
                string siteMatch = sitePrefix + computerSiteName;
                System.Collections.Generic.List<string> scpListNoSiteMatch =
                   new System.Collections.Generic.List<string>();

                foreach (System.DirectoryServices.SearchResult scpDirEntry in scpDirEntries)
                {
                    System.DirectoryServices.ResultPropertyValueCollection entryKeywords = scpDirEntry.Properties["keywords"];

                    // Identify SCP URLs.
                    if (Contains(entryKeywords, ScpUrlGuidString))
                    {
                        // Get the SCP URL.
                        scpUrl = scpDirEntry.Properties["serviceBindingInformation"][0] as string;

                        // If the SCP URL matches the exact ComputerSiteName.
                        if (Contains(entryKeywords, siteMatch))
                        {
                            // Priority 1 SCP URL. Add SCP URL to the list if it's not already there.
                            if (!Contains(scpUrlList, scpUrl))
                            {
                                scpUrlList.Add(scpUrl);
                            }
                        }

                        // No match between the SCP URL and the ComputerSiteName
                        else
                        {
                            bool hasSiteKeyword = false;

                            // Check if SCP URL entry has any keyword starting with "Site=" 
                            foreach (string keyword in entryKeywords)
                            {
                                hasSiteKeyword |= keyword.StartsWith(sitePrefix, StringComparison.OrdinalIgnoreCase);
                            }

                            // Add SCP URL to the scpListNoSiteMatch list if it's not already there.
                            if (!Contains(scpListNoSiteMatch, scpUrl))
                            {
                                // Priority 2 SCP URL. SCP entry doesn't have any "Site=<otherSite>" keywords, insert at the top of list.
                                if (!hasSiteKeyword)
                                {
                                    scpListNoSiteMatch.Insert(0, scpUrl);
                                }
                                // Priority 3 SCP URL. SCP entry has at least one "Site=<otherSite>" keyword, add to the end of list.
                                else
                                {
                                    scpListNoSiteMatch.Add(scpUrl);
                                }
                            }
                        }
                    }
                }

                // Append SCP URLs to the list. List contains:
                // Priority 1 URLs -- URLs with an exact match, "Site=<machineSite>"
                // Priority 2 URLs -- URLs without a match, no any "Site=<anySite>" in the entry
                // Priority 3 URLs -- URLs without a match, "Site=<nonMachineSite>"
                if (scpListNoSiteMatch.Count > 0)
                {
                    foreach (string url in scpListNoSiteMatch)
                    {
                        if (!Contains(scpUrlList, url))
                        {
                            scpUrlList.Add(url);
                        }
                    }
                }
            }

            // If no entries found, try fallBackLdapPath if it's non-empty.
            if (scpUrlList.Count == 0)
            {
                if (!string.IsNullOrEmpty(fallBackLdapPath))
                {
                    return GetScpUrlList(context, domainName, fallBackLdapPath, ref maxHops);
                }
            }

            // Return the list with 0 or more SCP URLs.
            return scpUrlList;
        }

        /// <summary>
        /// AutoDiscover e-mail address against a list of URLs.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="emailAddress">SMTP address</param>
        /// <param name="nc">Network credentials</param>
        /// <param name="urls">List of URLs to try</param>
        /// <param name="authoritative">URLs are from Active Directory</param>
        /// <param name="allowSelfSignedCertificates">Allow self signed certs flag</param>
        /// <param name="maxHops">The number of remaining Active Directory or e-mail address redirections</param>
        /// <returns>
        /// The successful response.
        /// </returns>
        private static AutoDiscoverResponseXml DiscoverTryUrls(
            Context context,
            string emailAddress,
            System.Net.NetworkCredential nc,
            System.Collections.Generic.List<string> urls,
            bool authoritative,
            bool allowSelfSignedCertificates,
            ref int maxHops)
        {
            AutoDiscoverResponseXml responseXml = null;

            foreach (string url in urls)
            {
                // Try each URL - send request to AutoDiscover and parse the response.
                responseXml = DiscoverTryUrl(context, emailAddress, nc, url, authoritative, allowSelfSignedCertificates);

                // Check if the response contains data.
                if ((IsOutlookDataReturned(responseXml) && (responseXml.OutlookData.Account != null)))
                {
                    // Check if the response contains a redirect.
                    if (responseXml.OutlookData.Account.Action == "redirectAddr")
                    {
                        // Retry AutoDiscover with new redirect e-mail address.
                        string redirectEmailAddress = responseXml.OutlookData.Account.RedirectAddr;
                        responseXml = Discover(context, redirectEmailAddress, nc, ref maxHops, allowSelfSignedCertificates);
                    }

                    // Outlook account data is returned by the AutoDiscover service - stop the search.
                    break;
                }

                // Check for error response.
                else
                {
                    if (IsErrorResponseReturned(responseXml))
                    {
                        Logger.WriteTraceWarning(context, "-! ErrorCode='" + responseXml.ErrorResponse.Error.ErrorCode +
                           "', Message='" + responseXml.ErrorResponse.Error.Message + "' is returned by Audiscover service");
                    }
                }
            }

            // Return the successful response.
            return responseXml;
        }

        /// <summary>
        /// Contact AutoDiscover service at the given URL with the given e-mail address.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="emailAddress">SMTP address</param>
        /// <param name="nc">Network credentials</param>
        /// <param name="url">A single URL to try</param>
        /// <param name="authoritative">URL is from Active Directory</param>
        /// <param name="allowSelfSignedCertificates">Allow self-signed certificates</param>
        /// <returns>
        /// The AutoDiscover response.
        /// </returns>
        private static AutoDiscoverResponseXml DiscoverTryUrl(
            Context context,
            string emailAddress,
            System.Net.NetworkCredential nc,
            string url,
            bool authoritative,
            bool allowSelfSignedCertificates)
        {
            AutoDiscoverResponseXml responseXml = null;

            try
            {
                // Custom handling of certificates.
                if (allowSelfSignedCertificates)
                {
                    // Set the callback to handle certificates.
                    System.Net.ServicePointManager.ServerCertificateValidationCallback += CertificateValidationCallBack;
                }

                // Create WebRequest based on credentials and URL.
                System.Net.WebRequest webRequest = System.Net.HttpWebRequest.Create(url);
                if (nc == null)
                    webRequest.UseDefaultCredentials = true;
                else
                    webRequest.Credentials = nc;

                // Create AutoDiscover request body to POST to AutoDiscover service.
                webRequest.Method = "POST";
                webRequest.ContentType = "text/xml";

                System.IO.Stream webRequestStream = webRequest.GetRequestStream();
                SerializeRequest(emailAddress, webRequestStream);
                webRequestStream.Close();

                // Create WebResponse.
                System.Net.WebResponse webResponse;

                // Send the AutoDiscover request, get the response.
                webResponse = webRequest.GetResponse();

                // Deserialize and get the AutoDiscover response.
                responseXml = DeserializeResponse(webResponse.GetResponseStream());
            }

            catch (System.Net.WebException e)
            {
                if (authoritative)
                {
                    Logger.WriteTraceWarning(context, e, "!! Error");
                }
                else
                {
                    Logger.WriteTraceInformation(context, "-! Info: " + e.ToString());
                }

                // Re-throw if the exception is irrecoverable
                Helpers.FilterIrrecoverableExceptions(e);
            }
            finally
            {
                if (allowSelfSignedCertificates)
                {
                    // Remove the callback.
                    System.Net.ServicePointManager.ServerCertificateValidationCallback -= CertificateValidationCallBack;
                }
            }

            // Return the AutoDiscover response.
            return responseXml;
        }

        /// <summary>
        /// Get redirection to SSL AutoDiscover URL from non-SSL URL.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="domainName">Domain name for redirect</param>
        /// <returns>Valid redirect SSL URL or null</returns>
        private static string GetRedirectUrl(Context context, string domainName)
        {
            string url = "http://autodiscover." + domainName + AutoDiscoverPath;

            try
            {
                // Create the HTTP Web request.
                System.Net.HttpWebRequest httpRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url);

                httpRequest.Method = "GET";
                httpRequest.AuthenticationLevel = System.Net.Security.AuthenticationLevel.None;
                httpRequest.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Anonymous;
                httpRequest.AllowAutoRedirect = false;
                httpRequest.PreAuthenticate = false;

                // Send the request and get the response.
                System.Net.HttpWebResponse httpResponse = (System.Net.HttpWebResponse)httpRequest.GetResponse();

                // Check if 301 or 302 codes are returned.
                if ((httpResponse.StatusCode == System.Net.HttpStatusCode.Moved) ||
                   (httpResponse.StatusCode == System.Net.HttpStatusCode.Redirect))
                {
                    // Get the redirect URL and verify that it's valid.
                    string redirectUrl = httpResponse.Headers["Location"];
                    bool isValidRedirectUrl = false;

                    if (!string.IsNullOrEmpty(redirectUrl))
                    {
                        System.UriBuilder urib = new System.UriBuilder(redirectUrl);

                        // Check if URI is SSL and that the path matches.
                        isValidRedirectUrl =
                           (string.Compare(urib.Scheme, "https", StringComparison.OrdinalIgnoreCase) == 0) &&
                           (string.Compare(urib.Path, AutoDiscoverPath, StringComparison.OrdinalIgnoreCase) == 0);
                    }

                    // Return the redirect URL.
                    if (isValidRedirectUrl)
                    {
                        return redirectUrl;
                    }
                }
            }

            catch (System.Net.WebException e)
            {
                Logger.WriteTraceWarning(context, e, "!! Exception");

                // Re-throw if the exception is irrecoverable
                Helpers.FilterIrrecoverableExceptions(e);
            }

            // No redirect URL available or it is invalid.
            return null;
        }

        /// <summary>
        /// Callback to handle SSL certificate.
        /// </summary>
        /// <param name="sender">Sender</param>
        /// <param name="certificate">Certificate</param>
        /// <param name="chain">Chain</param>
        /// <param name="sslPolicyErrors">SSL policy errors</param>
        /// <returns>true if certificate is OK</returns>
        private static bool CertificateValidationCallBack(
            object sender,
            System.Security.Cryptography.X509Certificates.X509Certificate certificate,
            System.Security.Cryptography.X509Certificates.X509Chain chain,
            System.Net.Security.SslPolicyErrors sslPolicyErrors)
        {
            // If no errors - exit.
            if (sslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
            {
                return true;
            }

            // If RemoteCertificateChainErrors, walk the chain to handle errors.
            if ((sslPolicyErrors & System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) != 0)
            {
                if (chain != null && chain.ChainStatus != null)
                {
                    foreach (System.Security.Cryptography.X509Certificates.X509ChainStatus status in chain.ChainStatus)
                    {
                        if ((certificate.Subject == certificate.Issuer) &&
                           (status.Status == System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.UntrustedRoot))
                        {
                            // Untrusted root for self-signed certificate is OK.
                            continue;
                        }
                        else
                        {
                            if (status.Status != System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.NoError)
                            {
                                Logger.WriteTraceWarning("!! Chain Error status='" + status.Status.ToString() +
                                          "', statusInformation='" + status.StatusInformation + "'");
                                // Fail on any other certificate error.
                                return false;
                            }
                        }
                    }
                }

                // No errors except untrusted root for self-signed certificates.
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Case insensitive check if the collection contains the string.
        /// </summary>
        /// <param name="collection">The collection of objects, only strings are checked</param>
        /// <param name="match">String to match</param>
        /// <returns>true, if match contains in the collection</returns>
        private static bool Contains(System.Collections.ICollection collection, string match)
        {
            foreach (object obj in collection)
            {
                string str = obj as string;
                if (str != null)
                {
                    if (string.Compare(str, match, StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Check maxHops, if less than one - throw exception, otherwise - decrement the counter.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="maxHops">Hops counter</param>
        private static void CheckAndDecrementHops(Context context, ref int maxHops)
        {
            if (maxHops <= 0)
            {
                Logger.WriteTraceWarning(context, "!! The number of hops exceeded the limit");
                throw new System.ArgumentOutOfRangeException("maxHops", "The number of hops exceeded the limit");
            }
            maxHops = maxHops - 1;
        }

        /// <summary>
        /// Check if AutoDiscoverResponse contains Outlook data.
        /// </summary>
        /// <param name="autoDiscoverResponse">AutoDiscover response to verify</param>
        /// <returns>true if OutlookDate is presented</returns>
        private static bool IsOutlookDataReturned(AutoDiscoverResponseXml autoDiscoverResponse)
        {
            return ((autoDiscoverResponse != null) &&
                  (autoDiscoverResponse.OutlookData != null));
        }

        /// <summary>
        /// Check if AutoDiscoverResponse contains Error data.
        /// </summary>
        /// <param name="autoDiscoverResponse">AutoDiscover response to verify</param>
        /// <returns>true if ErrorResponse is presented</returns>
        private static bool IsErrorResponseReturned(AutoDiscoverResponseXml autoDiscoverResponse)
        {
            return ((autoDiscoverResponse != null) &&
                  (autoDiscoverResponse.ErrorResponse != null));
        }

        /// <summary>
        /// Generate an AutoDiscover XML Request.
        /// </summary>
        /// <param name="emailAddress">E-mail address to AutoDiscover</param>
        /// <param name="requestStream">Output stream for serialized AutoDiscover request</param>
        private static void SerializeRequest(string emailAddress, System.IO.Stream requestStream)
        {
            AutoDiscoverRequestXml requestXml =
               AutoDiscoverRequestXml.NewRequest(emailAddress);

            System.Xml.Serialization.XmlSerializer xmlSerializer =
               new System.Xml.Serialization.XmlSerializer(typeof(AutoDiscoverRequestXml));

            xmlSerializer.Serialize(requestStream, requestXml);
        }

        /// <summary>
        /// Parse an AutoDiscover XML Response.
        /// </summary>
        /// <param name="responseStream">Stream to deserialize</param>
        /// <returns>Deserialized AutoDiscoverResponse class instance</returns>
        private static AutoDiscoverResponseXml DeserializeResponse(System.IO.Stream responseStream)
        {
            System.Xml.Serialization.XmlSerializer xmlDeserializer =
               new System.Xml.Serialization.XmlSerializer(typeof(AutoDiscoverResponseXml));

            return xmlDeserializer.Deserialize(responseStream) as AutoDiscoverResponseXml;
        }

        /// <summary>
        /// Returns local computer's site name.
        /// </summary>
        /// <returns>Name of the local site.</returns>
        private static string GetSiteName()
        {
            System.DirectoryServices.ActiveDirectory.ActiveDirectorySite site =
               System.DirectoryServices.ActiveDirectory.ActiveDirectorySite.GetComputerSite();
            return site.Name;
        }
    }

    #region AutoDiscover Request Xml


    /// <summary></summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Autodiscover", Namespace = AutoDiscover.OutlookRequestNamespace)]
    public class AutoDiscoverRequestXml
    {
        /// <summary></summary>
        [System.Xml.Serialization.XmlElement(IsNullable = false)]
        public RequestXml Request;

        /// <summary>Creates AutoDiscover XML request</summary>
        public static AutoDiscoverRequestXml NewRequest(string address)
        {
            RequestXml request = new RequestXml();
            request.EMailAddress = address;
            request.AcceptableResponseSchema = AutoDiscover.OutlookResponseNamespace;

            AutoDiscoverRequestXml outlookRequest = new AutoDiscoverRequestXml();
            outlookRequest.Request = request;

            return outlookRequest;
        }
    }

    /// <summary>Request container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Request", Namespace = AutoDiscover.OutlookRequestNamespace)]
    public class RequestXml
    {
        /// <summary>User SMTP address</summary>
        public string EMailAddress;

        /// <summary>The response schema, acceptable for a client</summary>
        public string AcceptableResponseSchema;
    }

    #endregion

#pragma warning disable 0649 // The types below are initialized by de-serializing web responses

    /// <summary>AutoDiscover response container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Autodiscover", Namespace = AutoDiscover.AutoDiscoverResponseNamespace)]
    public class AutoDiscoverResponseXml
    {
        /// <summary>Outlook data container</summary>
        [System.Xml.Serialization.XmlElement(IsNullable = false, ElementName = "Response", Namespace = AutoDiscover.OutlookResponseNamespace)]
        public OutlookDataXml OutlookData;

        /// <summary>Error response</summary>
        [System.Xml.Serialization.XmlElement(IsNullable = false, ElementName = "Response", Namespace = AutoDiscover.AutoDiscoverResponseNamespace)]
        public ErrorResponseXml ErrorResponse;
    }

    /// <summary>Outlook data response container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Response", Namespace = AutoDiscover.OutlookResponseNamespace)]
    public class OutlookDataXml
    {
        /// <summary>User data container</summary>
        public UserXml User;
        /// <summary>Account data container</summary>
        public AccountXml Account;
    }

    /// <summary>User container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "User", Namespace = AutoDiscover.OutlookResponseNamespace)]
    public class UserXml
    {
        /// <summary>User display name</summary>
        public string DisplayName;
        /// <summary>Exchange LegacyDN address</summary>
        public string LegacyDN;
        /// <summary>Deployment Id</summary>
        public string DeploymentId;
    }

    /// <summary>Account container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Account", Namespace = AutoDiscover.OutlookResponseNamespace)]
    public class AccountXml
    {
        /// <summary></summary>
        public string AccountType;
        /// <summary></summary>
        public string Action;
        /// <summary></summary>
        public string RedirectAddr;
        /// <summary></summary>
        [System.Xml.Serialization.XmlElement]
        public ProtocolXml[] Protocol;
        /// <summary></summary>
        public string SSL;
        /// <summary></summary>
        public string AuthPackage;
    }

    /// <summary>Protocol container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Protocol", Namespace = AutoDiscover.OutlookResponseNamespace)]
    public class ProtocolXml
    {
        /// <summary></summary>
        public string Type;
        /// <summary></summary>
        public string Server;
        /// <summary></summary>
        public string ServerDN;
        /// <summary></summary>
        public string ServerVersion;
        /// <summary></summary>
        public string MdbDN;
        /// <summary></summary>
        public string ASUrl;
        /// <summary></summary>
        public string OOFUrl;
        /// <summary></summary>
        public string OABUrl;
        /// <summary></summary>
        public string UMUrl;
        /// <summary></summary>
        public int Port;
        /// <summary></summary>
        public int DirectoryPort;
        /// <summary></summary>
        public int ReferralPort;
        /// <summary></summary>
        public string FBPublish;
        /// <summary></summary>
        public string SSL;
        /// <summary></summary>
        public string TTL;
        /// <summary></summary>
        public string AuthPackage;
        /// <summary></summary>
        public string CertPrincipalName;
        /// <summary></summary>
        [System.Xml.Serialization.XmlAnyElement]
        public object[] OtherXml;
    }

    /// <summary></summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Response", Namespace = AutoDiscover.AutoDiscoverResponseNamespace)]
    public class ErrorResponseXml
    {
        /// <summary></summary>
        public ErrorXml Error;
    }

    /// <summary>Error container</summary>
    [System.Xml.Serialization.XmlRoot(ElementName = "Error", Namespace = AutoDiscover.AutoDiscoverResponseNamespace)]
    public class ErrorXml
    {
        /// <summary></summary>
        [System.Xml.Serialization.XmlAttribute]
        public string Time;
        /// <summary></summary>
        [System.Xml.Serialization.XmlAttribute]
        public string Id;
        /// <summary></summary>
        public string ErrorCode;
        /// <summary></summary>
        public string Message;
        /// <summary></summary>
        public string DebugData;
    }

#pragma warning restore 0649
}
